iT邦幫忙

2026 iThome 鐵人賽

DAY 4
0
自我挑戰組

Data Engineer 下班後偷學 AI系列 第 4

可是老闆,那不是 Agent

  • 分享至 

  • xImage
  •  

至少理論上不是

在產品開發中整合 LLM API 就能說自己有用 Agent 了嗎?
最近在開發的產品需要呼叫 LLM API 去解析一些複雜的 content,但所有流程都是確定的,A 做完後做 B,失敗的話做 C 等等,依據一套預先定義好的規則行動,不會有意外,如果真的有意外,那就是程式沒寫好,看 fallback 怎麼處理了。偏偏老闆一直堅持這就是 Agent,那我也沒辦法,你說的都對。

根據 Anthropic 在 Building effective agents 中對 Agent 的定義,如果 LLM 沒有控制 workflow execution,例如單輪 LLM、classifier、普通 chatbot,就不算 Agent;Agent 會用 LLM 管理 workflow execution、動態選 Tool,並判斷何時完成、失敗時如何修正。

光譜上大概是這樣吧:

                    Workflow control by model / Agent autonomy

低  ───────────────────────────────────────────────────────────────→  高

Predefined Workflow          Hybrid / Agent-assisted          Agent
        │                              │                        │
流程由程式決定                  程式與模型共同決定流程          模型動態決定流程

其實大部分的任務本來就比較適合 Predefined Workflow,尤其是那些本來就有標準流程的,我們能用 AI 替換掉其中的某些比較困難或耗時的任務,光是這樣其實也已經夠有價值了。

Agentic Patterns

畢竟網路上已經有很多文章寫到爛掉了,有點不值一提,直接請 GPT 做一張介紹圖吧。

https://ithelp.ithome.com.tw/upload/images/20260918/201389399aY3dyyrpa.png

1. ReAct (Reasoning and Acting)

白話來說就是讓 LLM 想一步做一步看一步,應該是最簡單也是最廣為人知的架構,只要 Tool Calling 再加一個 loop 就可以做一個簡單的 Agent 了。
優缺點分明啊,他很適合走完一步才知道下一步要做什麼的任務,舉裡來說我跟 Agent 說我想查 DB 壞掉的原因,Agent 毫無頭緒啊,很難先規劃接下來的每一步。但有幾個明顯得缺點,第一點就是邊走邊想一定比較慢,第二點是比較沒有一個全局規劃,沒有 checklist 可能會被最新的結果帶跑,第三點比較嚴重,就是錯誤會累積,假設中間有一步做錯了沒被抓出來,那後面就是在錯誤的基礎上繼續跑。

2. Plan-and-Execute

先把所有步驟都規劃好,再逐步執行,跟 ReAct 算是完全反過來吧,實務上其實會兩個搭配使用,先做全局 Plan 之後交給 ReAct 去決定執行細節。

3. ReWOO (Reasoning without Observation)

概念上是把 ReAct 的 Observe 的階段省略掉,但實際上比較像 Plan-and-Execute

Plan-and-Execute 最大的不同的是,ReWOO 在規劃階段就把後續需要哪些 Evidence、Tool 以及彼此 dependency 一起寫出來,盡量避免每次 Tool 執行後都重新找 LLM reasoning。Plan-and-Execute 則沒有這麼明確的限制,想怎麼設計都行,可以說 ReWOO 是更強調 Tool dependency 與減少 LLM call 的 Plan-and-Execute

4. Reflexion

一句話就是,研究好夥伴,我的免費牛馬,給我試到我滿意為止。

寫個 ReAct

雖然有很多現成的 Agent 的框架可以用,但自己寫一下也是挺有趣。
就像前面說的,ReAct 就是 Tool Calling 再加一個 loop,來看看究竟是不是。

Function Calling

LLM 只會打嘴砲,要讓 agent 做事要先給他做事的能力,基本上只要回傳的是文字都行。

def get_full_name(short_name) -> str:
    short_name = short_name.lower()
    names = {
        "emma": "Emma Chen",
        "jack": "Jack Liu"
    }
    return names.get(short_name, f"找不到 {short_name} 的全名")

def get_phone(full_name: str) -> str:
    phones = {
        "Emma Chen": "0912-345-678",
        "Jack Liu": "0988-765-432"
    }
    return phones.get(full_name, f"找不到 {full_name} 的電話")

TOOLS: dict[str, Callable[..., str]] = {
    "get_full_name": get_full_name,
    "get_phone": get_phone
}

class Agent:
    def call_tool(self, tool_name: str, arguments: dict[str, Any]) -> str:
        tool_name = tool_name.strip()
        tool = TOOLS.get(tool_name)
        if tool is None:
            available = ", ".join(sorted(TOOLS))
            return f"Unknown tool {tool_name!r}. Available tools: {available}."

        try:
            return str(tool(**arguments))
        except Exception as exc:
            return f"Tool {tool_name!r} failed: {exc}"

PROMPT

接著參考 IBM 的這篇文章來寫一下 prompt:

SYSTEM_PROMPT = """
You have access to the following tools: 

- get_full_name(short_name: str) -> returns the user's full name,
- get_phone(full_name: str) -> returns that person's phone,

Use the following format:

Question: the input question you must answer.
Thought: you should always think before you take action.
Action: the action to take, should be one of [get_full_name, get_time, get_phone], and an json string for input, splited with ",", for example: get_full_name, {"short_name":"Emma Chen"}
Observation: What I will give you for the action result.
... (this Thought/Action/Observation can repeat N times, you should strictly follow the order of these 3 moves)
Thought: you now know the final answer
Final Answer: the final answer to the original input question

Question and Observation are inputs.
Thought, Action and Final Answer are your outputs, only output one from [Thought, Action, Final Answer] at a time.
Output final answer only when you can answer the Question.
"""

一個 loop

這裡故意不去設定 tooltext.format,看看能不能單純靠 PROMPT 來控制 Thought → Action → Observation 的流程。

class Agent(Agent):    
    def get_response(self, text: str):
        return client.responses.create(
            model="gpt-6-astra",
            input=text,
            instructions=SYSTEM_PROMPT
        )

    def start_task(self, input: str) -> str:
        content = f"Question: {input}"
        print(content)

        for _ in range(30):
            response = self.get_response(content)
            output_text = response.output_text.strip()
            content += output_text
            print(output_text)
            if "Final Answer:" in output_text:
                return

            elif output_text.startswith("Action:"):
                action, action_input = output_text[len("Action:") :].split(",")
                action = action.strip()
                action_input = json.loads(action_input.strip())
                observation = self.call_tool(action, action_input)
                next_input = f"Observation: {observation}"
                content += next_input
                print(next_input)
            elif output_text.startswith("Thought:"):
                next_input = output_text
            else:
                next_input = "Observation: Your output should starts with Thought, Action or Final Answer."
                content += next_input
                print(next_input)

        raise RuntimeError(f"Agent exceeded max_steps={self.max_steps}.")

結果

流程的確是完全照著設定好的 Pattern 走。
是不是覺得 Thought 在這裡很雞肋?的確這種簡單的任務好像根本不用思考,但假設是複雜點的任務,強迫模型思考是可以減少幻覺的,另外也有助於提升整個 workflow 的透明度。

agent = Agent()
agent.start_task("jack 和 kevin 的電話分別是?")

# Question: jack 和 kevin 的電話分別是?
# Thought: 我需要先查詢 Jack 和 Kevin 的全名,才能查找各自的電話。
# Action: get_full_name, {"short_name":"jack"}
# Observation: Jack Liu
# Thought: 接著查詢 Jack Liu 的電話。
# Action: get_phone, {"full_name":"Jack Liu"}
# Observation: 0988-765-432
# Thought: 接著需要查詢 Kevin 的全名,才能查找他的電話。
# Action: get_full_name, {"short_name":"kevin"}
# Observation: 找不到 kevin 的全名
# Final Answer: Jack Liu 的電話是 0988-765-432。目前找不到 Kevin 的全名,因此無法查詢他的電話;請提供 Kevin 的全名。

但你如果我換個方式問,像是:

agent.start_task("你認識Jack嗎?你知道怎麼連絡他嗎?他還欠我錢欸")

# Question: 你認識Jack嗎?你知道怎麼連絡他嗎?他還欠我錢欸
# Thought: 我需要先確認你指的是哪位 Jack,才能查詢他的聯絡方式。
# Action: get_full_name, {"short_name":"Jack"}
# Observation: Jack Liu
# Thought: 目前查到的姓名是 Jack Liu,接下來需要確認是否有他的聯絡方式。
# Action: get_phone, {"full_name":"Jack Liu"}
# Observation: 0988-765-432
# Final Answer: 根據你提供的查詢結果,Jack 的全名是 Jack Liu,電話是 0988-765-432。我不認識他本人,也無法確認號碼是否仍有效;聯絡時建議先確認身分,再平和地詢問還款安排。

雖然結果還是成功取得資訊,但顯然 Jack 完蛋了,為了守護 Jack 的錢包我們勢必得要做更多控制才行,單純自己寫挺麻煩的,所以是時候讓明天的我去試試框架了。

每日一句

不是 Agent 沒關係,簡報上是就好。


上一篇
從 FastAPI + Codex App Server 到 CLIProxyAPI
下一篇
目標讓 LangChain 成為 Pokémon 大師
系列文
Data Engineer 下班後偷學 AI8
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言